part of "../time.dart";

/// 一段时间间隔，但是和[Duration]不同，它保留了开头和结尾的时间戳
/// 目前假设如果管理一组时间间隔，那么它们不应该重复，这样可以简化工作量
class Period implements Jsonable {
  final DateTime begin;
  final DateTime end;
  Period(this.begin, this.end) {
    if (end <= begin) throw Bug("结束时间[$end]必须晚于开始时间[$begin]");
  }

  // 序列化与反序列化
  @override
  Json toJson() => {
        "begin": begin.toIso8601String(),
        "end": end.toIso8601String(),
      };

  factory Period.fromRaw(Json raw) {
    // 有可能已经有field使用了start，因此需要解析start
    late final DateTime begin;
    if (raw["start"] != null) {
      begin = DateTime.parse(raw["start"]);
    } else {
      begin = DateTime.parse(raw["begin"]);
    }
    return Period(
      begin,
      DateTime.parse(raw["end"]),
    );
  }

  /// 如果可能的话，这种序列化的方法比json更简单
  @override
  String toString() => "${begin.toIso8601String()},${end.toIso8601String()}";
  factory Period.fromString(String input) {
    DateTime begin = DateTime.parse(input.split(",").first);
    DateTime end = DateTime.parse(input.split(",").last);
    return Period(begin, end);
  }

  Duration get duration => end - begin;

  /// 时段的中点，比如用于粗略判断它属于哪个日期很好用
  DateTime get middlePoint =>
      begin + Duration(milliseconds: (duration.inMilliseconds / 2).round());

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    if (other is! Period) return false;
    return begin == other.begin && end == other.end;
  }

  @override
  int get hashCode => Object.hash(begin, end);
}

extension IntervalOperation on Period {
  /// 判断两段时间的先后顺序
  bool operator >(Period other) {
    // 如果其中一段的start小于另一段的end，则可以立马判断
    if (begin > other.end) {
      return true;
    } else if (end < other.begin) {
      return false;
    } else {
      // 除此以外，直接报错
      throw UnimplementedError("两段时间有重叠");
    }
  }

  /// 对大于取反
  bool operator <(Period other) => other > this;

  /// 判断一段时间是否属于某一天
  /// 定义标准：只要"擦到"就算属于
  bool isDate(Date date) {
    List<Period> fakePeriods = [this];
    Map<Date, List<Period>> grouped = fakePeriods.groupByDate();

    /// 如果分类后Date不为空，意思就是属于
    return grouped[date] != null;
  }
}

extension PeriodOperation on Period {
  /// 取2段时间的交集，如无交集，则返回null
  Period? intersect(Period other) {
    // 找出两个时间段中最晚的开始时间
    DateTime latestBegin = begin.isAfter(other.begin) ? begin : other.begin;

    // 找出两个时间段中最早的结束时间
    DateTime earliestEnd = end.isBefore(other.end) ? end : other.end;

    // 如果最晚的开始时间在最早的结束时间之后或者同时，那么没有交集
    if (latestBegin.isAfter(earliestEnd) ||
        latestBegin.isAtSameMomentAs(earliestEnd)) {
      return null;
    }

    // 否则，返回交集
    return Period(latestBegin, earliestEnd);
  }

  /// 将时间片段的开始和结尾都平移一段时间
  Period shift(Duration duration) {
    final newBegin = begin + duration;
    final newEnd = end + duration;
    return Period(newBegin, newEnd);
  }
}

extension PeriodStatistics on Iterable<Period> {
  /// 将整个List都与一段时间进行交集计算，并将有交集的部分返回，作为结果
  List<Period> intersect(Period other) {
    List<Period> intersections = [];

    for (Period period in this) {
      var intersection = other.intersect(period);
      if (intersection != null) {
        intersections.add(intersection);
      }
    }

    return intersections;
  }

  List<Period> intersectByDay(DateTime time, int boundaryHour) {
    // 获取当前时间所在自定义日的Period
    Period todayPeriod = getTimePeriod(time, boundaryHour);

    // 筛选出属于今天的番茄时间
    return intersect(todayPeriod);
  }

  /// 计算一组时间的时间总和（用来计算番茄或者总时间数）
  /// 此操作会丢失精确的时间戳信息
  Duration get duration => fold<Duration>(
        Duration.zero,
        (duration, period) => duration + period.duration,
      );

  Map<Date, List<Period>> groupByDate() {
    // 创建一个空的Map
    final Map<Date, List<Period>> result = {};

    // 临时定义一个添加间隔的方法
    addPeriod(Date date, Period period) {
      // 如果date不存在，则创建一个空的list
      if (result[date] == null) {
        result[date] = [];
      }

      // 在date中加入period
      result[date]!.add(period);
    }

    // 对于每一个Period，判断它横跨的日期
    for (final period in this) {
      var beginDate = Date.fromDateTime(period.begin);
      var endDate = Date.fromDateTime(period.end);

      // 如果是同一天，那么直接将Period加入当前日期
      if (beginDate == endDate) {
        addPeriod(beginDate, period);
      } else {
        // 如果不是同一天那么先将开始和结束分别加入Map中
        addPeriod(beginDate, period);
        addPeriod(endDate, period);

        // 将开始日期+1
        beginDate++;
        // 当开始和结束日期不同时，那么循环添加，直到日期相同为止
        // 如果开始和结束仅相差1天，那么会跳过这个循环
        while (beginDate != endDate) {
          addPeriod(beginDate, period);
          beginDate++;
        }
      }
    }

    return result;
  }

  /// 一组时段的中点，当List为空时，中点也为空
  DateTime? get middlePoint {
    // 如果没有记录，则直接返回空
    if (this.isEmpty) return null;

    // 将begin和end分别提取成List
    Iterable<DateTime> begins = map((e) => e.begin);
    Iterable<DateTime> ends = map((e) => e.end);

    // 从两个list中提取最早的和最晚的时间
    DateTime earliest = begins.earlist;
    DateTime latest = ends.latest;

    // 将最早和最晚组合成一个period
    final period = Period(earliest, latest);
    return period.middlePoint;
  }
}

/// 根据给定的时间和每日分界线小时数，返回该时间所在自定义日的Period
/// 根据给定的时间，获取它所在日期的Period
Period getTimePeriod(DateTime time, int divideHour) {
  // 确保分界小时在0到23之间
  assert(divideHour >= 0 && divideHour < 24);

  DateTime startOfDay;
  DateTime endOfDay;

  // 当前时间的分界线时间
  DateTime boundaryDateTime =
      DateTime(time.year, time.month, time.day, divideHour);

  if (time.isBefore(boundaryDateTime)) {
    // 如果当前时间在分界线之前，自定义日的开始是前一天的分界线时间
    startOfDay = boundaryDateTime.subtract(Duration(days: 1));
    endOfDay = boundaryDateTime;
  } else {
    // 如果当前时间在分界线之后或者正好是分界线时间，自定义日的开始是当天的分界线时间
    startOfDay = boundaryDateTime;
    endOfDay = boundaryDateTime.add(Duration(days: 1));
  }

  return Period(startOfDay, endOfDay);
}
